// Copyright (c) 2007 Readify Pty. Ltd.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
using BuildVirtualizer.ReservationManager;
using Microsoft.TeamFoundation.Build.Proxy;
using Microsoft.TeamFoundation.Client;
using BuildVirtualizer.Properties;
using Microsoft.TeamFoundation.Server;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.IO;
using System.Xml.Serialization;

namespace BuildVirtualizer
{
    public class BuildVirtualizerDispatcher
    {
        private static TraceSwitch m_TraceSwitch = new TraceSwitch(
            "General",
            string.Empty
            );

        private static BuildVirtualizerDispatcher m_Current;

        public static BuildVirtualizerDispatcher Current
        {
            get
            {
                if (BuildVirtualizerDispatcher.m_Current == null)
                {
                    BuildVirtualizerDispatcher.m_Current = new BuildVirtualizerDispatcher();
                    BuildVirtualizerDispatcher.Current.Start();
                }

                return m_Current;
            }
        }

        private Queue<BuildQueueItem> m_Queue = new Queue<BuildQueueItem>();

        public BuildQueueItem QueueBuild(string teamProject, string buildType)
        {
            BuildQueueItem item = new BuildQueueItem();
            item.TeamProject = teamProject;
            item.BuildType = buildType;
            item.BuildMachine = this.ResolveBuildMachine(teamProject, buildType);

            this.m_Queue.Enqueue(item);
            return item;
        }

        private string ResolveBuildMachine(string teamProject, string buildType)
        {
            #region Tracing
#line hidden
			if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceVerbose)
			{
				Trace.TraceInformation(
					"Resolving build machine for team project '{0}' build type '{1}'.",
                    teamProject,
                    buildType
					);
			}
#line default
			#endregion
	
            MappingCollection mappings = this.LoadMappingsFromVersionControl(teamProject);

            foreach (Mapping mapping in mappings)
            {
                if ((mapping.BuildType == buildType) && this.IsPermittedBuildMachine(mapping.BuildMachine))
                {
                    #region Tracing
#line hidden
			if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
			{
				Trace.TraceInformation(
					"A virtualization mapping was found. Build machine will be '{0}'.",
                    mapping.BuildMachine
					);
			}
#line default
			#endregion
	
                    return mapping.BuildMachine;
                }
            }

            #region Tracing
#line hidden
			if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
			{
				Trace.TraceInformation(
					"Failed to map a build machine, will try the default '{0}'.",
                    Settings.Default.DefaultBuildMachine
					);
			}
#line default
			#endregion

            return Settings.Default.DefaultBuildMachine;
        }

        private bool IsPermittedBuildMachine(string buildMachine)
        {
            #region Tracing
#line hidden
			if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceVerbose)
			{
				Trace.TraceInformation(
					"Checking that build machine '{0}' is permitted.",
                    buildMachine
					);
			}
#line default
			#endregion

            int index = Array.IndexOf<string>(Settings.Default.PermittedBuildMachines, buildMachine);
            if (index != -1)
            {
                return true;
            }

            #region Tracing
#line hidden
			if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceWarning)
			{
				Trace.TraceWarning(
					"The requested build machine '{0}' was not permitted. Naughty!",
                    buildMachine
					);
			}
#line default
			#endregion
	
            return false;
        }

        private MappingCollection LoadMappingsFromVersionControl(string teamProject)
        {
            #region Tracing
#line hidden
			if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceVerbose)
			{
				Trace.TraceInformation(
					"Downloading build virtualizer mapping file for team project '{0}'.",
                    teamProject
					);
			}
#line default
			#endregion

            MappingCollection mappings = null;

            string tempPath = Path.GetTempFileName();
            string serverItem = string.Format("$/{0}/TeamBuildTypes/BuildVirtualizer.xml", teamProject);

            try
            {
                VersionControlServer versionControl = TfsUtility.GetService<VersionControlServer>();
                versionControl.DownloadFile(serverItem, tempPath);

                #region Tracing
#line hidden
			    if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceVerbose)
			    {
				    Trace.TraceInformation(
					    "Successfully downloaded mapping file from '{0}' to '{1}'.",
                        serverItem,
                        tempPath
					    );
			    }
#line default
			    #endregion
	

            }
            catch (VersionControlException ex)
            {
                #region Tracing
#line hidden
			if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
			{
				Trace.TraceInformation(
					"Failed to download to mappings file from '{0}', that is OK, will return an empty one:\n{0}",
                    serverItem,
                    ex
					);
			}
#line default
			#endregion

                return new MappingCollection();
            }

            try
            {
                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceVerbose)
                {
                    Trace.TraceInformation(
                        "Deserializing virtualization mapping file '{0}'.",
                        tempPath
                        );
                }
#line default
                #endregion

                using (FileStream stream = new FileStream(tempPath, FileMode.Open))
                {
                    XmlSerializer serializer = new XmlSerializer(typeof(MappingCollection));
                    mappings = (MappingCollection)serializer.Deserialize(stream);
                }

                #region Tracing
#line hidden
			    if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
			    {
				    Trace.TraceInformation(
					    "Successfully deserialized virtualization mapping file '{0}', it contained '{1}' mappings.",
                        tempPath,
                        mappings.Count
					    );
			    }
#line default
    			#endregion
            }
            catch (Exception ex)
            {
                #region Tracing
#line hidden
			    if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceWarning)
			    {
				    Trace.TraceWarning(
					    "Virtualization mapping file '{0}' was downloaded but could not be deserialized. An empty mapping file will be returned:\n{0}",
                        serverItem,
                        ex
					    );
			    }
#line default
			    #endregion
                return new MappingCollection();
            }
            finally
            {
                File.Delete(tempPath);
            }

            return mappings;
        }

        private Thread m_WorkerThread;
        private BuildQueueItem m_CurrentBuildQueueItem;

        private void Worker()
        {
            while (true)
            {
                if (this.m_Queue.Count != 0)
                {
                    try
                    {
                        this.m_CurrentBuildQueueItem = this.m_Queue.Dequeue();
                        this.ProcessQueueBuildItem(this.m_CurrentBuildQueueItem);
                    }
                    catch (Exception ex)
                    {
                        #region Tracing
#line hidden
                        if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceError)
                        {
                            Trace.TraceError(
                                "Failed to process build queue item." +
                                "\nTeam Project = '{0}'" +
                                "\nBuild Type = '{1}'" +
                                "\nBuild URI = '{2}'" +
                                "\nTicket = '{3}'" +
                                "\nException = '{4}'",
                                this.m_CurrentBuildQueueItem.TeamProject,
                                this.m_CurrentBuildQueueItem.BuildType,
                                this.m_CurrentBuildQueueItem.BuildUri,
                                this.m_CurrentBuildQueueItem.Ticket,
                                ex
                                );
                        }
#line default
                        #endregion
                    }
                    finally
                    {
                        this.m_CurrentBuildQueueItem = null;
                    }
                }
                else
                {
                    Thread.Sleep(1000);
                }
            }
        }

        private void StopBuildIfNecessary(BuildData build)
        {
            BuildStore store = TfsUtility.GetService<BuildStore>();
            BuildController controller = TfsUtility.GetService<BuildController>();

            if (!this.IsBuildCompleted(build))
            {
                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Stopping build:" +
                        "\nTeam Project = '{0}'" +
                        "\nBuild Type = '{1}'" +
                        "\nBuild URI = '{2}'" +
                        "\nBuild Number = '{3}'",
                        build.TeamProject,
                        build.BuildType,
                        build.BuildUri,
                        build.BuildNumber
                        );
                }
#line default
                #endregion

                store.UpdateBuildFinishTime(build.BuildUri, DateTime.Now);
                store.UpdateBuildStatus(
                    build.BuildUri,
                    Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildStopped
                    );
                controller.BuildCompleted(build.BuildUri);
            }
        }

        private bool IsBuildCompleted(BuildData build)
        {
            return
                (build.BuildStatus == Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildSucceeded) ||
                (build.BuildStatus == Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildFailed) ||
                (build.BuildStatus == Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildStopped);
        }


        private ReservationManagerClient m_ReservationManager = new ReservationManagerClient();

        private void ProcessQueueBuildItem(BuildQueueItem item)
        {
            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Processing Build Queue Item." +
                    "\nTeam Project = '{0}'" +
                    "\nBuild Type = '{1}'",
                    this.m_CurrentBuildQueueItem.TeamProject,
                    this.m_CurrentBuildQueueItem.BuildType
                    );
            }
#line default
            #endregion

            this.EnsureAllBuildsStopped();
            this.ReserveVirtualMachine(item);
            this.WaitForReadyState(item);
            this.RunBuildAndWait(item);
            this.ReleaseVirtualMachine(item);

            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Finished Processing Build Queue Item." +
                    "\nTeam Project = '{0}'" +
                    "\nBuild Type = '{1}'",
                    this.m_CurrentBuildQueueItem.TeamProject,
                    this.m_CurrentBuildQueueItem.BuildType
                    );
            }
#line default
            #endregion
        }

        private void EnsureAllBuildsStopped()
        {
            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation("Ensuring all builds are stopped.");
            }
#line default
            #endregion

            ICommonStructureService structureService = TfsUtility.GetService<ICommonStructureService>();
            BuildStore storeService = TfsUtility.GetService<BuildStore>();

            ProjectInfo[] projects = structureService.ListAllProjects();

            foreach (ProjectInfo project in projects)
            {
                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation("Stopping builds for Team Project = '{0}'.", project.Name);
                }
#line default
                #endregion

                BuildData[] builds = storeService.GetListOfBuilds(project.Name, null);

                foreach (BuildData build in builds)
                {
                    this.StopBuildIfNecessary(build);
                }
            }
        }

        private void ReleaseVirtualMachine(BuildQueueItem item)
        {
            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Releasing virtual machine:" +
                    "\nTeam Project = '{0}'" +
                    "\nBuild Type = '{1}'" +
                    "\nTicket = '{2}'" +
                    "\nBuild URI = '{3}'",
                    item.TeamProject,
                    item.BuildType,
                    item.Ticket,
                    item.BuildUri
                    );
            }
#line default
            #endregion

            this.m_ReservationManager.Release(item.Ticket);
        }

        private void RunBuildAndWait(BuildQueueItem item)
        {
            DateTime startTime = DateTime.Now;

            item.BuildUri = this.StartBuild(item);

            while (true)
            {
                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Checking build status:" +
                        "\nTeam Project = '{0}'" +
                        "\nBuild Type = '{1}'" +
                        "\nTicket = '{2}'" +
                        "\nBuild URI = '{3}'",
                        item.TeamProject,
                        item.BuildType,
                        item.Ticket,
                        item.BuildUri
                        );
                }
#line default
                #endregion

                BuildStore store = TfsUtility.GetService<BuildStore>();
                BuildData data = store.GetBuildDetails(item.BuildUri);
                if ((data.BuildStatus == Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildSucceeded) ||
                    (data.BuildStatus == Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildStopped) ||
                    (data.BuildStatus == Microsoft.TeamFoundation.Build.Common.BuildConstants.BuildStatus.BuildFailed))
                {
                    #region Tracing
#line hidden
                    if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
                    {
                        Trace.TraceInformation(
                            "Checking build status:" +
                            "\nTeam Project = '{0}'" +
                            "\nBuild Type = '{1}'" +
                            "\nTicket = '{2}'" +
                            "\nBuild URI = '{3}'",
                            item.TeamProject,
                            item.BuildType,
                            item.Ticket,
                            item.BuildUri
                            );
                    }
#line default
                    #endregion

                    return;
                }

                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Checking reservation status:" +
                        "\nTeam Project = '{0}'" +
                        "\nBuild Type = '{1}'" +
                        "\nTicket = '{2}'" +
                        "\nBuild URI = '{3}'",
                        item.TeamProject,
                        item.BuildType,
                        item.Ticket,
                        item.BuildUri
                        );
                }
#line default
                #endregion

                ReservationStatus status = this.m_ReservationManager.Inquire(item.Ticket);
                if (status != ReservationStatus.Ready) throw new Exception("Reservation status is invalid"); // TODO: Look at exception handling.
                if (this.IsBuildTakingTooLong(startTime)) throw new Exception("Build is taking too long."); // TODO: Look at exception handling.

                Thread.Sleep(5000);
            }
        }

        private bool IsBuildTakingTooLong(DateTime startTime)
        {
            TimeSpan buildDuration = DateTime.Now - startTime;
            if (buildDuration > Settings.Default.BuildTimeout) return true;
            return false;
        }

        private string StartBuild(BuildQueueItem item)
        {
            BuildParameters parameters = new BuildParameters();
            parameters.BuildDirectory = Settings.Default.BuildDirectory;
            parameters.BuildMachine = item.BuildMachine;
            parameters.TeamFoundationServer = Settings.Default.TeamFoundationServer;
            parameters.TeamProject = item.TeamProject;
            parameters.BuildType = item.BuildType;

            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Starting build:" +
                    "\nTeam Project = '{0}'" +
                    "\nBuild Type = '{1}'" +
                    "\nTicket = '{2}'" +
                    "\nBuild Directory = '{3}'" +
                    "\nBuild Machine = '{4}'",
                    item.TeamProject,
                    item.BuildType,
                    item.Ticket,
                    parameters.BuildDirectory,
                    parameters.BuildMachine
                    );
            }
#line default
            #endregion

            BuildController controller = TfsUtility.GetService<BuildController>();
            string buildUri = controller.StartBuild(parameters);

            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Build started successfully:" +
                    "\nTeam Project = '{0}'" +
                    "\nBuild Type = '{1}'" +
                    "\nBuild URI = '{2}'" +
                    "\nTicket = '{3}'",
                    item.TeamProject,
                    item.BuildType,
                    item.BuildUri,
                    item.Ticket
                    );
            }
#line default
            #endregion

            return buildUri;
        }

        private void WaitForReadyState(BuildQueueItem item)
        {
            DateTime startTime = DateTime.Now;

            while (true)
            {
                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Waiting for Ready state:" +
                        "\nTeam Project = '{0}'" +
                        "\nBuild Type = '{1}'" +
                        "\nTicket = '{2}'",
                        item.TeamProject,
                        item.BuildType,
                        item.Ticket
                        );
                }
#line default
                #endregion

                ReservationStatus status = this.m_ReservationManager.Inquire(item.Ticket);
                if (status == ReservationStatus.Ready) return;

                if (status == ReservationStatus.Error) throw new Exception("Error on the reservation manager."); // TODO: Look at exception handling in general.
                if (this.HasExceededQueueTimeout(startTime)) throw new Exception("Queue timeout!"); // TODO: look at exception handling in general.

                Thread.Sleep(5000);
            }
        }

        private bool HasExceededQueueTimeout(DateTime startTime)
        {
            TimeSpan durationInQueue = DateTime.Now - startTime;
            if (durationInQueue > Settings.Default.QueueTimeout) return true; // TODO: This should be configurable.
            return false;
        }

        private void ReserveVirtualMachine(BuildQueueItem item)
        {
            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Reserving build machine:" +
                    "\nTeam Project = '{0}'" +
                    "\nBuild Type = '{1}'" +
                    "\nBuild Machine = '{2}'",
                    item.TeamProject,
                    item.BuildType,
                    item.BuildMachine
                    );
            }
#line default
            #endregion

            string ticket = this.m_ReservationManager.Reserve(item.BuildMachine);
            item.Ticket = ticket;

            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation(
                    "Reservation granted:" +
                    "\nTeam Project = '{0}'" +
                    "\nBuild Type = '{1}'" +
                    "\nTicket = '{2}'",
                    item.TeamProject,
                    item.BuildType,
                    item.Ticket
                    );
            }
#line default
            #endregion
        }

        public void Start()
        {
            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation("Starting build virtualizer dispatcher.");
            }
#line default
            #endregion

            if (this.m_WorkerThread == null)
            {
                this.m_WorkerThread = new Thread(this.Worker);
                this.m_WorkerThread.IsBackground = true;
                this.m_WorkerThread.Start();
            }
            else
            {
                throw new InvalidOperationException("The dispatcher has already been started.");
            }
        }

        public void Stop()
        {
            #region Tracing
#line hidden
            if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
            {
                Trace.TraceInformation("Stopping build virtualizer dispatcher.");
            }
#line default
            #endregion

            if (this.m_WorkerThread != null)
            {
                this.m_WorkerThread.Abort();
                this.m_WorkerThread = null;
            }
            else
            {
                throw new InvalidOperationException("The dispatcher was not started.");
            }
        }

        public bool StopBuild(string teamProject, string buildNumber, string buildUri)
        {
            string[] buildNumberParts = buildNumber.Split('_');
            string buildType = buildNumberParts[0];

            if (this.m_CurrentBuildQueueItem.BuildType == buildType && this.m_CurrentBuildQueueItem.TeamProject == teamProject)
            {
                this.ForwardStopBuild(this.m_CurrentBuildQueueItem, buildNumber);
            }

            return true;
        }

        private void ForwardStopBuild(BuildQueueItem buildQueueItem, string buildNumber)
        {
            try
            {
                string buildAgentUri = string.Format(
                    "tcp://{0}:9191/{1}",
                    buildQueueItem.BuildMachine,
                    typeof(Microsoft.TeamFoundation.Build.Common.IBuildAgent).FullName
                    );

                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceWarning)
                {
                    Trace.TraceWarning(
                        "Request to forward a stop build request was received." +
                        "\nTeam Project = '{0}'" +
                        "\nBuild Type = '{1}'" +
                        "\nBuild URI = '{2}'" +
                        "\nBuild Agent URI = '{3}'",
                        buildQueueItem.TeamProject,
                        buildNumber,
                        buildQueueItem.BuildUri,
                        buildAgentUri
                        );
                }
#line default
                #endregion

                Microsoft.TeamFoundation.Build.Common.IBuildAgent agent =
                    (Microsoft.TeamFoundation.Build.Common.IBuildAgent)Activator.GetObject(
                    typeof(Microsoft.TeamFoundation.Build.Common.IBuildAgent),
                    buildAgentUri
                    );
                bool result = agent.StopBuild(
                    buildQueueItem.TeamProject,
                    buildNumber,
                    buildQueueItem.BuildUri
                    );

                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceInfo)
                {
                    Trace.TraceInformation(
                        "Forwarded stop build request outcome was '{5}'." +
                        "\nTeam Project = '{0}'" +
                        "\nBuild Type = '{1}'" +
                        "\nBuild URI = '{2}'" +
                        "\nBuild Agent URI = '{3}'",
                        buildQueueItem.TeamProject,
                        buildNumber,
                        buildQueueItem.BuildUri,
                        buildAgentUri,
                        result
                        );
                }
#line default
                #endregion
            }
            catch (Exception ex)
            {
                // We are doing this (swallowing) because it may often fail because of the
                // virtualization hacks that are going on. But we forward it anyway just
                // in case it can clean-up nicely.
                #region Tracing
#line hidden
                if (BuildVirtualizerDispatcher.m_TraceSwitch.TraceWarning)
                {
                    Trace.TraceWarning(
                        "Forwarded stop build request failed." +
                        "\nTeam Project = '{0}'" +
                        "\nBuild Number = '{2}'" +
                        "\nBuild URI = '{3}'" +
                        "\nException = '{4}'",
                        buildQueueItem.TeamProject,
                        buildNumber,
                        buildQueueItem.BuildUri,
                        ex
                        );
                }
#line default
                #endregion
            }
        }
    }
}
